feat: Implement ALT + F4 and Window X button to close game#2336
feat: Implement ALT + F4 and Window X button to close game#2336githubawn wants to merge 21 commits intoTheSuperHackers:mainfrom
Conversation
|
| Filename | Overview |
|---|---|
| Core/GameEngine/Source/GameClient/GUI/LoadScreen.cpp | Refactors all inline ESC-key / serviceWindowsOS calls in video playback loops to use the new GameClient::isMovieAbortRequested(). Adds the early-return guard after the ChallengeLoadScreen min-spec frame-ready wait to prevent frameDecompress on a non-ready frame. The SinglePlayerLoadScreen #if RTS_GENERALS min-spec wait loop (line ~599) still has no abort check, but frames complete quickly there so the impact is minimal. |
| Generals/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp | Adds the central quit() method, wraps startNewGame() with a try/catch for QuitGameException, and adds the QuitGameException throw in updateLoadProgress(). The quit() method correctly restores the !isInSkirmishGame() guard for MSG_SELF_DESTRUCT and guards TheScriptEngine calls with loading-state checks. Logic is sound. |
| GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp | Mirror of the Generals GameLogic.cpp changes. Identical quit() implementation, QuitGameException mechanism, and startNewGame() wrapper. Parity with the Generals version is maintained. |
| Generals/Code/Main/WinMain.cpp | WM_CLOSE now posts MSG_META_DEMO_INSTANT_QUIT with a boolean force argument instead of hard-exiting. Adds proper null guards for TheGameEngine. The isAltF4 detection via GetAsyncKeyState is unreliable at dispatch time and will nearly always be false in practice, meaning Alt+F4 during gameplay shows the quit menu rather than force-quitting. |
| GeneralsMD/Code/Main/WinMain.cpp | WM_CLOSE and WM_QUERYENDSESSION both updated to append MSG_META_DEMO_INSTANT_QUIT with a boolean argument and add null guards. Same GetAsyncKeyState reliability concern as the Generals version. WM_QUERYENDSESSION now guards TheGameEngine access in the else branch, addressing the prior null-dereference concern. |
| Generals/Code/GameEngine/Source/GameClient/GameClient.cpp | Implements isMovieAbortRequested(): checks ESC key, then calls serviceWindowsOS()/propagateMessages() and checks quitting/quit-to-desktop flags. Also adds the abort check in the 4-second intro movie loop. Clean consolidation of previously duplicated logic. |
Sequence Diagram
sequenceDiagram
participant OS as Windows OS
participant WndProc
participant MsgStream as MessageStream
participant CmdXlat as CommandTranslator
participant GL as GameLogic::quit()
participant LS as LoadScreen::update()
Note over OS,LS: Alt+F4 / Window X during loading
OS->>WndProc: WM_CLOSE
WndProc->>MsgStream: appendMessage(MSG_META_DEMO_INSTANT_QUIT, force)
Note over WndProc: isAltF4 via GetAsyncKeyState (may be unreliable)
LS->>LS: serviceWindowsOS()
LS->>MsgStream: propagateMessages()
MsgStream->>CmdXlat: translateGameMessage()
CmdXlat->>GL: quit(toDesktop=TRUE, force)
GL->>GL: m_quitToDesktopAfterMatch = TRUE
GL-->>CmdXlat: return
CmdXlat-->>MsgStream: DESTROY_MESSAGE
MsgStream-->>LS: return
LS->>LS: check getQuitting() || isQuitToDesktopRequested()
LS-->>GL: updateLoadProgress() throws QuitGameException
GL->>GL: catch QuitGameException in startNewGame()
Note over GL: Loading cleanly aborted
Note over OS,LS: Alt+F4 / Window X during movie
OS->>WndProc: WM_CLOSE
WndProc->>MsgStream: appendMessage(MSG_META_DEMO_INSTANT_QUIT, force)
loop isMovieAbortRequested() poll
LS->>LS: serviceWindowsOS() + propagateMessages()
LS->>GL: isQuitToDesktopRequested() → TRUE
LS-->>LS: break out of movie loop
end
Prompt To Fix All With AI
This is a comment left during a code review.
Path: Generals/Code/Main/WinMain.cpp
Line: 376-378
Comment:
**Alt+F4 detection via `GetAsyncKeyState` is unreliable**
`GetAsyncKeyState` polls the instantaneous key state at the time of the call. However, `WM_CLOSE` is *posted* (not sent) to the message queue by `DefWindowProc` in response to `WM_SYSKEYDOWN` for Alt+F4. By the time the pump dequeues and dispatches `WM_CLOSE`, the user has almost certainly already released both keys, so `altDown && f4Down` evaluates to `false` in the vast majority of real presses.
In practice this means `isAltF4` is nearly always `false` during active gameplay, so Alt+F4 will show the quit-confirmation menu rather than force-quitting — identical to the window X-button path. The same issue is present in `GeneralsMD/Code/Main/WinMain.cpp` at the equivalent lines.
A more reliable approach is to track the key-combination in a flag set during `WM_SYSKEYDOWN` (before `WM_CLOSE` is enqueued):
```cpp
// near the top of WndProc or in a static:
static Bool s_altF4Requested = FALSE;
case WM_SYSKEYDOWN:
if (wParam == VK_F4 && (lParam & (1 << 29))) // bit 29 = ALT held
s_altF4Requested = TRUE;
break;
case WM_CLOSE:
if (TheGameEngine && !TheGameEngine->getQuitting())
{
Bool isAltF4 = s_altF4Requested;
s_altF4Requested = FALSE;
...
}
```
This flag is set synchronously during the same key-event cycle and is reliable regardless of how long the message pump takes.
How can I resolve this? If you propose a fix, please make it concise.Reviews (15): Last reviewed commit: "move variable and include to more logica..." | Re-trigger Greptile
GeneralsMD/Code/GameEngine/Source/GameClient/GUI/LoadScreen.cpp
Outdated
Show resolved
Hide resolved
|
Needs rebase. My initial thought on this, without looking at code, is that it will make it easier for players to disconnect from a Multiplayer match - by accident or intentional - so this feature must not work during a Multiplayer Match. |
|
Will rebase soon. The multiplayer behavior was designed based on your comment from #1356 (comment). It also adds safe abort logic so Alt+F4 works consistently everywhere addressing helmutbuhler's #1356 (comment) concern about the button only working in some situations. |
|
Thank you for implementing this. I did a few quick tests and the current implementation appears to work well. |
843b370 to
4fda496
Compare
GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp
Outdated
Show resolved
Hide resolved
GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp
Outdated
Show resolved
Hide resolved
2d896d5 to
036ed84
Compare
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
xezon
left a comment
There was a problem hiding this comment.
It would have been easier to review if this change just contained Core+GeneralsMD edits for starters.
GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp
Outdated
Show resolved
Hide resolved
GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp
Outdated
Show resolved
Hide resolved
GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp
Outdated
Show resolved
Hide resolved
GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp
Outdated
Show resolved
Hide resolved
3a3c926 to
6f2b0b8
Compare
a2166e4 to
c6ee106
Compare
Additional Comments (1)
The original code called The identical implementation in Prompt To Fix With AIThis is a comment left during a code review.
Path: Generals/Code/GameEngine/Source/GameClient/GameClient.cpp
Line: 803-806
Comment:
**`TheKeyboard->UPDATE()` called on every invocation of `isMovieAbortRequested()`**
`isMovieAbortRequested()` is called in tight video playback loops (e.g. spinning on `isFrameReady()` with `Sleep(1)` between iterations). Each call unconditionally invokes `TheKeyboard->UPDATE()`, which may accumulate keyboard state incorrectly or cause events to be double-processed when the function is called at high frequency within a single logical frame.
The original code called `UPDATE()` once per outer loop iteration for a specific keyboard check. The refactored helper should either guard against repeated calls (e.g. a static frame counter check) or the callers should call it once per outer iteration rather than inside the innermost spin loop.
The identical implementation in `GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp` has the same concern.
How can I resolve this? If you propose a fix, please make it concise. |
moved trystartnewgame to private
fixed MSG_SELF_DESTRUCT sent for single-player loading abort
GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogic.cpp
Outdated
Show resolved
Hide resolved
GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp
Show resolved
Hide resolved
added comments moved QuitGameException
|
Greptile is still complaining about "the SinglePlayerLoadScreen min-spec wait loop in LoadScreen.cpp was not given the same isMovieAbortRequested() + frame-ready guard that ChallengeLoadScreen received, leaving a small hang window during Generals-only min-spec map loads." |
|
Greptile didn't update the first comment and I don't think I can tag it as it doesn't give me the tag pop-up. Let me try: |
Summary of PR #2336: Alt+F4 and Window X Button SupportThis PR implements graceful exit handling for Alt+F4 and the window close button, addressing issue #1356. The changes are split across two commits: Commit 1: Centralized Quit Logic
Commit 2: Graceful Exit During Loading & Movies
Technical Details
The implementation ensures clean shutdown in all game states (main menu, loading, in-game, during movies) while properly handling multiplayer, recording, and script engine cleanup. |
|
@greptileai Include "Key findings:" and "Confidence Score:". |
|
Key findings:
Minor observations:
Confidence Score: 90/100 This is high-quality work. The architecture is sound, edge cases are handled, and the refactoring meaningfully improves maintainability. The 10% deduction is purely because extensive quit scenarios (loading screens, movies, multiplayer, recording, etc.) require thorough testing to ensure no regressions. |
GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp
Outdated
Show resolved
Hide resolved
fixes accidentally revert
| { | ||
| TheMessageStream->appendMessage(GameMessage::MSG_META_DEMO_INSTANT_QUIT); | ||
| // TheSuperHackers @info ThePlayerList initialized ensures we can safely append messages. | ||
| // If not, we're likely in early startup/late shutdown, so we hard-quit instead. |
There was a problem hiding this comment.
What the comment describes is unintuitive. Where/how have you seen this requirement?
There was a problem hiding this comment.
this was added after this comment
|
|
||
| //This method used to disable quitting. We just put up the options screen instead. | ||
| //TheMessageStream->appendMessage(GameMessage::MSG_META_OPTIONS); | ||
| if (TheMessageStream && ThePlayerList) |
There was a problem hiding this comment.
This one is also testing ThePlayerList. I still do not understand it. It looks like a "code smell".
There was a problem hiding this comment.
This is for the exact same reason as above. It prevents the engine from crashing during an early exit since ThePlayerList isn't ready.
Fixes #1356
Split up in 2 commits.
Previously, the quit logic was duplicated and inconsistent across several places (the quit menu, the "quit to desktop" button, and the Alt+F4 handler). This commit centralizes all of that into a single GameLogic::quit(Bool toDesktop) method. It properly handles edge cases like stopping an active recording, sending a self-destruct message in multiplayer (using a new m_quitToDesktopAfterMatch flag), unpausing/unfreezing the game before exiting. All the old scattered quit code is replaced with a simple call to TheGameLogic->quit(true/false).
This follow-up extends the graceful exit to two more situations where the game previously couldn't be closed cleanly:
During the loading screen: The load progress update now checks if a quit has been requested and throws a QuitGameException, which is caught in startNewGame() to cleanly abort the loading process mid-way.
During movies: The inline ESC-key checking that was copy-pasted in multiple video playback loops is refactored into a new GameClient::isMovieAbortRequested() method. This method now also checks for window close / Alt+F4 events (not just ESC), so closing the window during a movie no longer gets stuck. The MSG_META_DEMO_INSTANT_QUIT message is also registered as a system message so it propagates correctly even during loading.